Cargo templating for `new` and `init`
authorEwan Higgs <ewan.higgs@hgst.com>
Tue, 16 Aug 2016 23:58:05 +0000 (01:58 +0200)
committerEwan Higgs <ewan_higgs@yahoo.co.uk>
Tue, 31 Jan 2017 09:23:42 +0000 (10:23 +0100)
PR #3004 This is a resubmission of the PR #1747 (from scratch) which adds
support for templating in Cargo. The templates are implemented using the
handlebars crate (where the original PR used mustache).

Examples:
cargo new --template https://url/to/template somedir foo
cargo new --template https://url/to/templates --template-subdir somedir foo
cargo new --template ../path/to/template somedir foo

16 files changed:
Cargo.lock
Cargo.toml
src/bin/init.rs
src/bin/new.rs
src/cargo/lib.rs
src/cargo/ops/cargo_new.rs
src/cargo/sources/git/mod.rs
src/cargo/sources/git/utils.rs
src/cargo/util/errors.rs
src/cargo/util/mod.rs
src/cargo/util/paths.rs
src/cargo/util/template.rs [new file with mode: 0644]
src/doc/guide.md
src/etc/_cargo
src/etc/cargo.bashcomp.sh
tests/new.rs

index 1f5d9b9e850bcdaf8b9f17c498b15a325c3e1735..1ed69cc0a1a60a7227e7a7a9b3c95a27bf48af18 100644 (file)
@@ -17,6 +17,7 @@ dependencies = [
  "git2-curl 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "hamcrest 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "handlebars 0.20.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
  "libgit2-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -236,6 +237,19 @@ dependencies = [
  "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "handlebars"
+version = "0.20.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "idna"
 version = "0.1.0"
@@ -255,6 +269,11 @@ dependencies = [
  "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "lazy_static"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "lazy_static"
 version = "0.2.2"
@@ -453,6 +472,11 @@ dependencies = [
  "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "pest"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "pkg-config"
 version = "0.3.8"
@@ -467,6 +491,11 @@ dependencies = [
  "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "quick-error"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "rand"
 version = "0.3.14"
@@ -651,8 +680,10 @@ dependencies = [
 "checksum git2-curl 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "68676bc784bf0bef83278898929bf64a251e87c0340723d0b93fa096c9c5bf8e"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum hamcrest 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bf088f042a467089e9baa4972f57f9247e42a0cc549ba264c7a04fbb8ecb89d4"
+"checksum handlebars 0.20.5 (registry+https://github.com/rust-lang/crates.io-index)" = "07f9c1d28bcfb97143c95ed0667141677b2b5675c7ba3d5b81459ad43b1073bd"
 "checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
 "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417"
 "checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b"
 "checksum libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "a51822fc847e7a8101514d1d44e354ba2ffa7d4c194dcab48870740e327cac70"
 "checksum libgit2-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "502e50bcdcfa98df366bdd54935bff856f4cf11f725daa608092c0288205887a"
@@ -675,8 +706,10 @@ dependencies = [
 "checksum openssl 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1eb2a714828f5528e4a24a07c296539216f412364844d61fe1161f94558455d4"
 "checksum openssl-probe 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "756d49c8424483a3df3b5d735112b4da22109ced9a8294f1f5cdf80fb3810919"
 "checksum openssl-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "95e9fb08acc32509fac299d6e5f4932e1e055bb70d764282c3ed8beaa87ab0e9"
+"checksum pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8"
 "checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa"
 "checksum psapi-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abcd5d1a07d360e29727f757a9decb3ce8bc6e0efa8969cfaad669a8317a2478"
+"checksum quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0aad603e8d7fb67da22dbdf1f4b826ce8829e406124109e73cf1b2454b93a71c"
 "checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
 "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
 "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
index 54ca09385dcdbf7cd07bb15f854c70985b84275f..906ef725de1f6593e81d871ab1ead8cc100d9ee8 100644 (file)
@@ -28,9 +28,11 @@ fs2 = "0.3"
 git2 = "0.6"
 git2-curl = "0.7"
 glob = "0.2"
+handlebars = "0.20"
 libc = "0.2"
 libgit2-sys = "0.6"
 log = "0.3"
+miow = "0.1"
 num_cpus = "1.0"
 regex = "0.1"
 rustc-serialize = "0.3"
index 336ebe387cb703d222fa8af35ea9c38b2ac00cb5..53cb8f0730c4244f0d3d1eafeedf66272899b7e6 100644 (file)
@@ -12,6 +12,8 @@ pub struct Options {
     flag_lib: bool,
     arg_path: Option<String>,
     flag_name: Option<String>,
+    flag_template_subdir: Option<String>,
+    flag_template: Option<String>,
     flag_vcs: Option<ops::VersionControl>,
     flag_frozen: bool,
     flag_locked: bool,
@@ -32,6 +34,8 @@ Options:
     --bin               Use a binary (application) template
     --lib               Use a library template
     --name NAME         Set the resulting package name
+    --template <repository>  Use a specified template repository
+    --template-subdir <template>  Use a specified template within a template repository
     -v, --verbose ...   Use verbose output (-vv very verbose/build.rs output)
     -q, --quiet         No output printed to stdout
     --color WHEN        Coloring: auto, always, never
@@ -47,14 +51,22 @@ pub fn execute(options: Options, config: &Config) -> CliResult {
                      options.flag_frozen,
                      options.flag_locked)?;
 
-    let Options { flag_bin, flag_lib, arg_path, flag_name, flag_vcs, .. } = options;
+    let Options { 
+        flag_bin, flag_lib, 
+        arg_path, flag_name, 
+        flag_vcs, 
+        flag_template_subdir, flag_template, 
+        .. 
+    } = options;
 
     let tmp = &arg_path.unwrap_or(format!("."));
     let opts = ops::NewOptions::new(flag_vcs,
                                      flag_bin,
                                      flag_lib,
                                      tmp,
-                                     flag_name.as_ref().map(|s| s.as_ref()));
+                                     flag_name.as_ref().map(|s| s.as_ref()),
+                                     flag_template_subdir.as_ref().map(|s| s.as_ref()),
+                                     flag_template.as_ref().map(|s| s.as_ref()));
 
     let opts_lib = opts.lib;
     ops::init(opts, config)?;
index 865985e367dfd83f8ec5c1c89f2e889356b0521d..deb80f2a1edec3333618bbc35227543225cb7c63 100644 (file)
@@ -12,6 +12,8 @@ pub struct Options {
     flag_lib: bool,
     arg_path: String,
     flag_name: Option<String>,
+    flag_template_subdir: Option<String>,
+    flag_template: Option<String>,
     flag_vcs: Option<ops::VersionControl>,
     flag_frozen: bool,
     flag_locked: bool,
@@ -32,6 +34,8 @@ Options:
     --bin               Use a binary (application) template
     --lib               Use a library template
     --name NAME         Set the resulting package name, defaults to the value of <path>
+    --template <repository>  Use a specified template repository
+    --template-subdir <template-subdir>  Use a specified template within a template repository
     -v, --verbose ...   Use verbose output (-vv very verbose/build.rs output)
     -q, --quiet         No output printed to stdout
     --color WHEN        Coloring: auto, always, never
@@ -47,13 +51,21 @@ pub fn execute(options: Options, config: &Config) -> CliResult {
                      options.flag_frozen,
                      options.flag_locked)?;
 
-    let Options { flag_bin, flag_lib, arg_path, flag_name, flag_vcs, .. } = options;
+    let Options { 
+        flag_bin, flag_lib, 
+        arg_path, flag_name, 
+        flag_vcs, 
+        flag_template_subdir, flag_template, 
+        .. 
+    } = options;
 
     let opts = ops::NewOptions::new(flag_vcs,
                                     flag_bin,
                                     flag_lib,
                                     &arg_path,
-                                    flag_name.as_ref().map(|s| s.as_ref()));
+                                    flag_name.as_ref().map(|s| s.as_ref()),
+                                    flag_template_subdir.as_ref().map(|s| s.as_ref()),
+                                    flag_template.as_ref().map(|s| s.as_ref()));
 
     let opts_lib = opts.lib;
     ops::new(opts, config)?;
index 822b668e27d84e92a0a6b07355cf2d25aec6b28e..991a661386ab000bfad50a68776c7ff0920a3ffb 100755 (executable)
@@ -12,6 +12,7 @@ extern crate flate2;
 extern crate fs2;
 extern crate git2;
 extern crate glob;
+extern crate handlebars;
 extern crate libc;
 extern crate libgit2_sys;
 extern crate num_cpus;
index 985d2f41add0e7de74c85c285ed01b149231e917..9f9a0f494705ce71fcf5ee6cf7e954f9f2babfb8 100644 (file)
@@ -1,19 +1,22 @@
 use std::env;
-use std::fs;
-use std::path::Path;
+use std::fs::{self, DirEntry, File};
+use std::path::{Path, PathBuf};
 use std::collections::BTreeMap;
-
 use rustc_serialize::{Decodable, Decoder};
 
 use git2::Config as GitConfig;
 
 use term::color::BLACK;
 
+use handlebars::{Handlebars, Context, no_escape};
+use tempdir::TempDir;
+
 use core::Workspace;
+use sources::git::clone;
 use util::{GitRepo, HgRepo, CargoResult, human, ChainError, internal};
-use util::{Config, paths};
-
-use toml;
+use util::{Config, paths, template};
+use util::template::{TemplateSet, TemplateFile, TemplateDirectory, TemplateType};
+use util::template::{InputFileTemplateFile, InMemoryTemplateFile, get_template_type};
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum VersionControl { Git, Hg, NoVcs }
@@ -24,6 +27,8 @@ pub struct NewOptions<'a> {
     pub lib: bool,
     pub path: &'a str,
     pub name: Option<&'a str>,
+    pub template_subdir: Option<&'a str>,
+    pub template: Option<&'a str>,
 }
 
 struct SourceFileInformation {
@@ -34,9 +39,10 @@ struct SourceFileInformation {
 
 struct MkOptions<'a> {
     version_control: Option<VersionControl>,
+    template_subdir: Option<&'a str>,
+    template: Option<&'a str>,
     path: &'a Path,
     name: &'a str,
-    source_files: Vec<SourceFileInformation>,
     bin: bool,
 }
 
@@ -59,7 +65,9 @@ impl<'a> NewOptions<'a> {
            bin: bool,
            lib: bool,
            path: &'a str,
-           name: Option<&'a str>) -> NewOptions<'a> {
+           name: Option<&'a str>,
+           template_subdir: Option<&'a str>,
+           template: Option<&'a str>) -> NewOptions<'a> {
 
         // default to lib
         let is_lib = if !bin {
@@ -75,6 +83,8 @@ impl<'a> NewOptions<'a> {
             lib: is_lib,
             path: path,
             name: name,
+            template_subdir: template_subdir,
+            template: template,
         }
     }
 }
@@ -85,6 +95,52 @@ struct CargoNewConfig {
     version_control: Option<VersionControl>,
 }
 
+fn get_input_template(config: &Config, opts: &MkOptions) -> CargoResult<TemplateSet> {
+    let name = opts.name;
+
+    let template_type = try!(get_template_type(opts.template, opts.template_subdir));
+    let template_set = match template_type {
+        // given template is a remote git repository & needs to be cloned
+        TemplateType::GitRepo(repo_url) => {
+            let template_dir = try!(TempDir::new(name));
+            config.shell().status("Cloning", &repo_url)?;
+            clone(&repo_url, &template_dir.path(), &config)?;
+            let template_path = find_template_subdir(&template_dir.path(), opts.template_subdir);
+            TemplateSet {
+                template_dir: Some(TemplateDirectory::Temp(template_dir)),
+                template_files: try!(collect_template_dir(&template_path, opts.path))
+            }
+        },
+        // given template is a local directory
+        TemplateType::LocalDir(directory) => {
+            // make sure that the template exists
+            if fs::metadata(&directory).is_err() {
+                bail!("template `{}` not found", directory);
+            }
+            let template_path = find_template_subdir(&PathBuf::from(&directory),
+                                                     opts.template_subdir);
+            TemplateSet {
+                template_dir: Some(TemplateDirectory::Normal(PathBuf::from(directory))),
+                template_files: try!(collect_template_dir(&template_path, opts.path))
+            }
+        },
+        // no template given, use either "lib" or "bin" templates depending on the
+        // presence of the --bin flag.
+        TemplateType::Builtin => {
+            let template_files = if opts.bin {
+                create_bin_template()
+            } else {
+                create_lib_template()
+            };
+            TemplateSet {
+                template_dir: None,
+                template_files: template_files
+            }
+        }
+    };
+    Ok(template_set)
+}
+
 fn get_name<'a>(path: &'a Path, opts: &'a NewOptions, config: &Config) -> CargoResult<&'a str> {
     if let Some(name) = opts.name {
         return Ok(name);
@@ -277,9 +333,10 @@ pub fn new(opts: NewOptions, config: &Config) -> CargoResult<()> {
 
     let mkopts = MkOptions {
         version_control: opts.version_control,
+        template_subdir: opts.template_subdir,
+        template: opts.template,
         path: &path,
         name: name,
-        source_files: vec![plan_new_source_file(opts.bin, name.to_string())],
         bin: opts.bin,
     };
 
@@ -343,10 +400,11 @@ pub fn init(opts: NewOptions, config: &Config) -> CargoResult<()> {
 
     let mkopts = MkOptions {
         version_control: version_control,
+        template_subdir: opts.template_subdir,
+        template: opts.template,
         path: &path,
         name: name,
         bin: src_paths_types.iter().any(|x|x.bin),
-        source_files: src_paths_types,
     };
 
     mk(config, &mkopts).chain_error(|| {
@@ -410,7 +468,7 @@ fn mk(config: &Config, opts: &MkOptions) -> CargoResult<()> {
 
     let (author_name, email) = discover_author()?;
     // Hoo boy, sure glad we've got exhaustivenes checking behind us.
-    let author = match (cfg.name, cfg.email, author_name, email) {
+    let author = match (cfg.name.clone(), cfg.email.clone(), author_name, email) {
         (Some(name), Some(email), _, _) |
         (Some(name), None, _, Some(email)) |
         (None, Some(email), name, _) |
@@ -419,73 +477,50 @@ fn mk(config: &Config, opts: &MkOptions) -> CargoResult<()> {
         (None, None, name, None) => name,
     };
 
-    let mut cargotoml_path_specifier = String::new();
-
-    // Calculare what [lib] and [[bin]]s do we need to append to Cargo.toml
-
-    for i in &opts.source_files {
-        if i.bin {
-            if i.relative_path != "src/main.rs" {
-                cargotoml_path_specifier.push_str(&format!(r#"
-[[bin]]
-name = "{}"
-path = {}
-"#, i.target_name, toml::Value::String(i.relative_path.clone())));
-            }
-        } else {
-            if i.relative_path != "src/lib.rs" {
-                cargotoml_path_specifier.push_str(&format!(r#"
-[lib]
-name = "{}"
-path = {}
-"#, i.target_name, toml::Value::String(i.relative_path.clone())));
-            }
-        }
-    }
-
-    // Create Cargo.toml file with necessary [lib] and [[bin]] sections, if needed
-
-    paths::write(&path.join("Cargo.toml"), format!(
-r#"[package]
-name = "{}"
-version = "0.1.0"
-authors = [{}]
-
-[dependencies]
-{}"#, name, toml::Value::String(author), cargotoml_path_specifier).as_bytes())?;
-
-
-    // Create all specified source files
-    // (with respective parent directories)
-    // if they are don't exist
-
-    for i in &opts.source_files {
-        let path_of_source_file = path.join(i.relative_path.clone());
-
-        if let Some(src_dir) = path_of_source_file.parent() {
-            fs::create_dir_all(src_dir)?;
+    // construct the mapping used to populate the template
+    // if in the future we want to make more varaibles available in
+    // the templates, this would be the place to do it.
+    let mut handlebars = Handlebars::new();
+    // We don't want html escaping unless users explicitly ask for it...
+    handlebars.register_escape_fn(no_escape);
+    handlebars.register_helper("toml-escape", Box::new(template::toml_escape_helper));
+    handlebars.register_helper("html-escape", Box::new(template::html_escape_helper));
+
+    let mut data = BTreeMap::new();
+    data.insert("name".to_owned(), name.to_owned());
+    data.insert("author".to_owned(), author);
+
+    let template_set = try!(get_input_template(config, opts));
+    for template in template_set.template_files.iter() {
+        let template_str = try!(template.template());
+        let dest_path = path.join(template.path());
+
+        // Skip files that already exist.
+        if fs::metadata(&dest_path).is_ok() {
+            continue;
         }
 
-        let default_file_content : &[u8] = if i.bin {
-            b"\
-fn main() {
-    println!(\"Hello, world!\");
-}
-"
-        } else {
-            b"\
-#[cfg(test)]
-mod tests {
-    #[test]
-    fn it_works() {
-    }
-}
-"
-        };
-
-        if !fs::metadata(&path_of_source_file).map(|x| x.is_file()).unwrap_or(false) {
-            paths::write(&path_of_source_file, default_file_content)?;
-        }
+        let parent = try!(dest_path.parent()
+                          .chain_error(|| {
+                              human(format!("failed to make sure parent directory \
+                                             exists for {}", dest_path.display()))
+                          }));
+        try!(fs::create_dir_all(&parent)
+             .chain_error(|| {
+                 human(format!("failed to create path to destination file {}",
+                               parent.display()))
+             }));
+
+        // create the new file & render the template to it
+        let mut dest_file = try!(File::create(&dest_path).chain_error(|| {
+                                     human(format!("failed to open file for writing: {}",
+                                                   dest_path.display()))
+                                 }));
+
+        try!(handlebars.template_renderw(&template_str, &Context::wraps(&data), &mut dest_file)
+            .chain_error(|| {
+                human(format!("Failed to render template for file: {}", dest_path.display()))
+        }))
     }
 
     if let Err(e) = Workspace::new(&path.join("Cargo.toml"), config) {
@@ -493,10 +528,36 @@ mod tests {
                            workspace configuration\n\n{}", e);
         config.shell().warn(msg)?;
     }
-
     Ok(())
 }
 
+// When the command line has --template=<repository-or-directory> and
+// --template-subdir=<template-name> then find_template_subdir fixes up the name as appropriate.
+fn find_template_subdir(template_dir: &Path, template: Option<&str>) -> PathBuf {
+    match template {
+        Some(template) => template_dir.join(template),
+        None => template_dir.to_path_buf()
+    }
+}
+
+fn collect_template_dir(template_path: &PathBuf, _: &Path) -> CargoResult<Vec<Box<TemplateFile>>> {
+    let mut templates = Vec::<Box<TemplateFile>>::new();
+    // For every file found inside the given template directory, compile it as a handlebars
+    // template and render it with the above data to a new file inside the target directory
+    try!(walk_template_dir(&template_path, &mut |entry| {
+        let entry_path = entry.path();
+        let dest_file_name = PathBuf::from(try!(entry_path.strip_prefix(&template_path)
+                                  .chain_error(|| {
+                                      human(format!("entry is somehow not a subpath \
+                                                     of the directory being walked."))
+                                  })));
+        templates.push(Box::new(InputFileTemplateFile::new(entry_path, 
+                                                           dest_file_name.to_path_buf())));
+        Ok(())
+    }));
+    Ok(templates)
+}
+
 fn get_environment_variable(variables: &[&str] ) -> Option<String>{
     variables.iter()
              .filter_map(|var| env::var(var).ok())
@@ -548,6 +609,7 @@ fn global_config(config: &Config) -> CargoResult<CargoNewConfig> {
         }
         None => None
     };
+
     Ok(CargoNewConfig {
         name: name,
         email: email,
@@ -555,6 +617,84 @@ fn global_config(config: &Config) -> CargoResult<CargoNewConfig> {
     })
 }
 
+/// Recursively list directory contents under `dir`, only visiting files.
+///
+/// This will also filter out files & files types which we don't want to
+/// try generate templates for. Image files, for instance.
+///
+/// It also filters out certain files & file types, as we don't want t
+///
+/// We use this instead of std::fs::walk_dir as it is marked as unstable for now
+///
+/// This is a modified version of the example at:
+///    http://doc.rust-lang.org/std/fs/fn.read_dir.html
+fn walk_template_dir(dir: &Path, cb: &mut FnMut(DirEntry) -> CargoResult<()>) -> CargoResult<()> {
+    let attr = try!(fs::metadata(&dir));
+    let ignore_files = vec![".gitignore"];
+
+    if !attr.is_dir() {
+        return Ok(());
+    }
+    for entry in try!(fs::read_dir(dir)) {
+        let entry = try!(entry);
+        let attr = try!(fs::metadata(&entry.path()));
+        if attr.is_dir() {
+            if let Some(ref path_str) = entry.path().to_str() {
+                if !&path_str.contains(".git") {
+                    try!(walk_template_dir(&entry.path(), cb));
+                }
+            }
+        } else {
+            if let Some(file_name) = entry.path().file_name() {
+                if ignore_files.contains(&file_name.to_str().unwrap()) {
+                    continue
+                }
+            }
+            try!(cb(entry));
+        }
+    }
+    Ok(())
+}
+
+/// Create a generic template
+///
+/// This consists of a Cargo.toml, and a src directory.
+fn create_generic_template() -> Vec<Box<TemplateFile>> {
+    let template_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("Cargo.toml"),
+    String::from(r#"[package]
+name = "{{name}}"
+version = "0.1.0"
+authors = [{{toml-escape author}}]
+
+[dependencies]
+"#)));
+    vec![template_file]
+}
+
+/// Create a new "lib" project
+fn create_lib_template() -> Vec<Box<TemplateFile>> {
+    let mut template_files = create_generic_template();
+    let lib_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("src/lib.rs"),
+    String::from(r#"#[test]
+fn it_works() {
+}
+"#)));
+    template_files.push(lib_file);
+    template_files
+}
+
+/// Create a new "bin" project
+fn create_bin_template() -> Vec<Box<TemplateFile>> {
+    let mut template_files = create_generic_template();
+    let main_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("src/main.rs"),
+String::from("fn main() {
+    println!(\"Hello, world!\");
+}
+")));
+    template_files.push(main_file);
+    template_files
+}
+
 #[cfg(test)]
 mod tests {
     use super::strip_rust_affixes;
index 0ef4db4d6681bc199b7c25d8fb980832c57977c9..46827e7e49db3452124a377c6b1405c679fd97da 100644 (file)
@@ -1,4 +1,4 @@
-pub use self::utils::{GitRemote, GitDatabase, GitCheckout, GitRevision, fetch};
+pub use self::utils::{GitRemote, GitDatabase, GitCheckout, GitRevision, fetch, clone};
 pub use self::source::{GitSource, canonicalize_url};
 mod utils;
 mod source;
index 3b3b005f3a03baec770160f878c838ae947c3f80..c354960cab1244354ef3fd64406c04a9754003c6 100644 (file)
@@ -598,3 +598,21 @@ pub fn fetch(repo: &git2::Repository,
         Ok(())
     })
 }
+
+/// Clone a remote repository into a target directory. This is a simple utility function to get
+/// HEAD. When this is complete it should be equivalent to `git clone $url $target`
+pub fn clone(url: &str, target: &Path, config: &Config) -> CargoResult<()> {
+    let repo = git2::Repository::init(target).chain_error(||{
+        human(format!("Failed to create template directory `{}`",
+                      target.display()))
+    })?;
+    let refspec = "refs/heads/*:refs/heads/*";
+    fetch(&repo, &url, refspec, &config).chain_error(||{
+        human(format!("failed to fecth `{}`", url))
+    })?;
+    let reference = "HEAD";
+    let oid = repo.refname_to_id(reference)?;
+    let object = repo.find_object(oid, None)?;
+    repo.reset(&object, git2::ResetType::Hard, None)?;
+    Ok(())
+}
index c54199f5b15346771eddd32556b8f2b1a2d0966c..581c98c6845ea670727d65378dceb8e0ff3e442b 100644 (file)
@@ -3,12 +3,14 @@ use std::ffi;
 use std::fmt;
 use std::io;
 use std::num;
+use std::path;
 use std::process::{Output, ExitStatus};
 use std::str;
 use std::string;
 
 use curl;
 use git2;
+use handlebars;
 use rustc_serialize::json;
 use semver;
 use term;
@@ -343,6 +345,9 @@ from_error! {
     term::Error,
     num::ParseIntError,
     str::ParseBoolError,
+    path::StripPrefixError,
+    handlebars::TemplateRenderError,
+    handlebars::RenderError,
 }
 
 impl From<string::ParseError> for Box<CargoError> {
@@ -371,6 +376,9 @@ impl CargoError for ffi::NulError {}
 impl CargoError for term::Error {}
 impl CargoError for num::ParseIntError {}
 impl CargoError for str::ParseBoolError {}
+impl CargoError for path::StripPrefixError {}
+impl CargoError for handlebars::TemplateRenderError {}
+impl CargoError for handlebars::RenderError {}
 
 // =============================================================================
 // Construction helpers
index 0f9e92051b76aee96d11b9cb6e863cf4e6560fec..ccf013ec5ed663c2b05c5304ccac84f200757854 100644 (file)
@@ -32,6 +32,7 @@ pub mod network;
 pub mod paths;
 pub mod process_builder;
 pub mod profile;
+pub mod template;
 pub mod to_semver;
 pub mod to_url;
 pub mod toml;
index d47598a2ece148aab39d562967182408cd50954b..075a55a473f3695f42fba31e37356382308ad8d4 100644 (file)
@@ -2,6 +2,7 @@ use std::env;
 use std::ffi::{OsStr, OsString};
 use std::fs::File;
 use std::fs::OpenOptions;
+use std::io;
 use std::io::prelude::*;
 use std::path::{Path, PathBuf, Component};
 
@@ -67,6 +68,10 @@ pub fn without_prefix<'a>(a: &'a Path, b: &'a Path) -> Option<&'a Path> {
     }
 }
 
+pub fn file(p: &Path, contents: &[u8]) -> io::Result<()> {
+    try!(File::create(p)).write_all(contents)
+}
+
 pub fn read(path: &Path) -> CargoResult<String> {
     (|| -> CargoResult<_> {
         let mut ret = String::new();
diff --git a/src/cargo/util/template.rs b/src/cargo/util/template.rs
new file mode 100644 (file)
index 0000000..6b08b31
--- /dev/null
@@ -0,0 +1,238 @@
+use std::path::{Path, PathBuf};
+use std::fs::File;
+use std::io::{Read};
+
+use util::{CargoResult, human, ChainError};
+use url::Url;
+
+use handlebars::{Context, Helper, Handlebars, RenderContext, RenderError, html_escape};
+use tempdir::TempDir;
+use toml;
+
+/// toml_escape_helper quotes strings in templates when they are wrapped in
+/// {{#toml-escape <template-variable}}
+/// So if 'name' is "foo \"bar\"" then:
+/// {{name}} renders as  'foo "bar"'
+/// {{#toml-escape name}} renders as '"foo \"bar\""'
+pub fn toml_escape_helper(_: &Context,
+                          h: &Helper,
+                          _: &Handlebars,
+                          rc: &mut RenderContext) -> Result<(), RenderError> {
+    if let Some(param) = h.param(0) {
+        let txt = param.value().as_string().unwrap_or("").to_owned();
+        let rendered = format!("{}", toml::Value::String(txt));
+        try!(rc.writer.write(rendered.into_bytes().as_ref()));
+    }
+    Ok(())
+}
+
+/// html_escape_helper escapes strings in templates using html escaping rules.
+pub fn html_escape_helper(_: &Context,
+                          h: &Helper,
+                          _: &Handlebars,
+                          rc: &mut RenderContext) -> Result<(), RenderError> {
+    if let Some(param) = h.param(0) {
+        let rendered = html_escape(param.value().as_string().unwrap_or(""));
+        try!(rc.writer.write(rendered.into_bytes().as_ref()));
+    }
+    Ok(())
+}
+
+/// Trait to hold information required for rendering templated files.
+pub trait TemplateFile {
+    /// Path of the template output for the file being written.
+    fn path(&self) -> &Path;
+
+    /// Return the template string.
+    fn template(&self) -> CargoResult<String>;
+}
+
+/// TemplateFile based on an input file.
+pub struct InputFileTemplateFile {
+    input_path: PathBuf,
+    output_path: PathBuf,
+}
+
+impl TemplateFile for InputFileTemplateFile {
+    fn path(&self) -> &Path {
+        &self.output_path
+    }
+
+    fn template(&self) -> CargoResult<String> {
+        let mut template_str = String::new();
+        let mut entry_file = try!(File::open(&self.input_path).chain_error(|| {
+            human(format!("Failed to open file for templating: {}", self.input_path.display()))
+        }));
+        try!(entry_file.read_to_string(&mut template_str).chain_error(|| {
+            human(format!("Failed to read file for templating: {}", self.input_path.display()))
+        }));
+        Ok(template_str)
+    }
+}
+
+impl InputFileTemplateFile {
+    pub fn new(input_path: PathBuf, output_path: PathBuf) -> InputFileTemplateFile {
+        InputFileTemplateFile {
+            input_path: input_path,
+            output_path: output_path
+        }
+    }
+}
+
+/// An in memory template file for --bin or --lib.
+pub struct InMemoryTemplateFile {
+    template_str: String,
+    output_path: PathBuf,
+}
+
+impl TemplateFile for InMemoryTemplateFile {
+    fn path(&self) -> &Path {
+        &self.output_path
+    }
+
+    fn template(&self) -> CargoResult<String> {
+        Ok(self.template_str.clone())
+    }
+}
+
+impl InMemoryTemplateFile {
+    pub fn new(output_path: PathBuf, template_str: String) -> InMemoryTemplateFile {
+        InMemoryTemplateFile {
+            template_str: template_str,
+            output_path: output_path
+        }
+    }
+}
+
+pub enum TemplateDirectory{
+    Temp(TempDir),
+    Normal(PathBuf),
+}
+
+impl TemplateDirectory {
+    pub fn path(&self) -> &Path {
+        match *self {
+            TemplateDirectory::Temp(ref tempdir) => tempdir.path(),
+            TemplateDirectory::Normal(ref path) => path.as_path()
+        }
+    }
+}
+
+/// A listing of all the files that are part of the template.
+pub struct TemplateSet {
+    pub template_dir: Option<TemplateDirectory>,
+    pub template_files: Vec<Box<TemplateFile>>
+}
+
+// The type of template we will use.
+#[derive(Debug, Eq, PartialEq)]
+pub enum TemplateType  {
+    GitRepo(String),
+    LocalDir(String),
+    Builtin
+}
+
+/// Given a repository string and subdir, determine if this is a git repository, local file, or a
+/// built in template. Git only supports a few schemas, so anything that is not supported is
+/// treated as a local path. The supported schemes are:
+/// "git", "file", "http", "https", and "ssh"
+/// Also supported is an scp style syntax: git@domain.com:user/path
+pub fn get_template_type<'a>(repo: Option<&'a str>,
+                             subdir: Option<&'a str>) -> CargoResult<TemplateType> {
+    match (repo, subdir) {
+        (Some(repo_str), _) => {
+            if let Ok(repo_url) = Url::parse(&repo_str) {
+                let supported_schemes = ["git", "file", "http", "https", "ssh"];
+                if supported_schemes.contains(&repo_url.scheme()) {
+                    Ok(TemplateType::GitRepo(repo_url.into_string()))
+                } else {
+                    Ok(TemplateType::LocalDir(String::from(repo_str)))
+                }
+            } else {
+                Ok(TemplateType::LocalDir(String::from(repo_str)))
+            }
+        },
+        (None, Some(_)) => Err(human("A template was given, but no template repository")),
+        (None, None) => Ok(TemplateType::Builtin)
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use std::collections::BTreeMap;
+    use handlebars::Handlebars;
+    use super::*;
+
+    #[test]
+    fn test_toml_escape_helper() {
+        let mut handlebars = Handlebars::new();
+        handlebars.register_helper("toml-escape", Box::new(toml_escape_helper));
+        let mut data = BTreeMap::new();
+        data.insert("name".to_owned(), "\"Iron\" Mike Tyson".to_owned());
+        let result = handlebars.template_render("Hello, {{#toml-escape name}}{{/toml-escape}}", &data).unwrap();
+        assert_eq!(result, "Hello, \"\\\"Iron\\\" Mike Tyson\"");
+    }
+
+    macro_rules! test_get_template_proto {
+        ( $funcname:ident, $url:expr ) => {
+            #[test]
+            fn $funcname() {
+                assert_eq!(get_template_type(Some($url), Some("foo")).unwrap(),
+                TemplateType::GitRepo($url.to_owned()));
+                assert_eq!(get_template_type(Some($url), Some("")).unwrap(),
+                TemplateType::GitRepo($url.to_owned()));
+                assert_eq!(get_template_type(Some($url), None).unwrap(),
+                TemplateType::GitRepo($url.to_owned()));
+            }
+        }
+    }
+
+    test_get_template_proto!(test_get_template_http, "http://foo.com/user/repo");
+    test_get_template_proto!(test_get_template_https, "https://foo.com/user/repo");
+    test_get_template_proto!(test_get_template_git, "git://foo.com/user/repo");
+    test_get_template_proto!(test_get_template_file, "file://foo.com/user/repo");
+    test_get_template_proto!(test_get_template_ssh, "ssh://user@foo.com/repo");
+    // SSH scp style repository access is not yet supported.
+    //test_get_template_proto!(test_get_template_ssh_scp_style, "git@foo.com:user/repo");
+
+    #[test]
+    fn test_get_template_type_git_repo_bad_proto_is_localdir() {
+        assert_eq!(get_template_type(Some("ftps://foo.com/user/repo"), None).unwrap(),
+                   TemplateType::LocalDir("ftps://foo.com/user/repo".to_owned()));
+    }
+
+    #[test]
+    fn test_get_template_type_local_dir_abs() {
+        assert_eq!(get_template_type(Some("/foo/user/repo"), Some("foo")).unwrap(),
+                   TemplateType::LocalDir("/foo/user/repo".to_owned()));
+        assert_eq!(get_template_type(Some("/foo/user/repo"), Some("")).unwrap(),
+                   TemplateType::LocalDir("/foo/user/repo".to_owned()));
+        assert_eq!(get_template_type(Some("/foo/user/repo"), None).unwrap(),
+                   TemplateType::LocalDir("/foo/user/repo".to_owned()));
+    }
+
+    // Windows paths can be parsed as URLs so make sure they are parsed as local directories.
+    #[test]
+    fn test_get_template_type_windows_path_is_localdir() {
+        assert_eq!(get_template_type(Some(r#"C:\foo\user\repo"#), None).unwrap(),
+                   TemplateType::LocalDir(r#"C:\foo\user\repo"#.to_owned()));
+        assert_eq!(get_template_type(Some(r#"C:/foo/user/repo"#), None).unwrap(),
+                   TemplateType::LocalDir(r#"C:/foo/user/repo"#.to_owned()));
+    }
+
+    #[test]
+    fn test_get_template_type_local_dir_rel() {
+        assert_eq!(get_template_type(Some("foo/user/repo"), Some("foo")).unwrap(),
+                   TemplateType::LocalDir("foo/user/repo".to_owned()));
+        assert_eq!(get_template_type(Some("foo/user/repo"), Some("")).unwrap(),
+                   TemplateType::LocalDir("foo/user/repo".to_owned()));
+        assert_eq!(get_template_type(Some("foo/user/repo"), None).unwrap(),
+                   TemplateType::LocalDir("foo/user/repo".to_owned()));
+    }
+
+    #[test]
+    fn test_get_template_type_builtin() {
+        assert_eq!(get_template_type(None, None).unwrap(), TemplateType::Builtin);
+    }
+}
index 32ff74e8f7d5954b0ce13addc904e11bb127bcbe..d955a52b2bf192ea7067cc9b35bf9134d0c65884 100644 (file)
@@ -27,6 +27,9 @@ We’re passing `--bin` because we’re making a binary program: if we
 were making a library, we’d leave it off. This also initializes a new `git`
 repository by default. If you don't want it to do that, pass `--vcs none`.
 
+You can also use your own template to scaffold cargo projects! See the
+[Templates](#templates) section for more details.
+  
 Let’s check out what Cargo has generated for us:
 
 ```shell
@@ -49,6 +52,8 @@ we need to get started. First, let’s check out `Cargo.toml`:
 name = "hello_world"
 version = "0.1.0"
 authors = ["Your Name <you@example.com>"]
+
+[dependencies]
 ```
 
 This is called a **manifest**, and it contains all of the metadata that Cargo
@@ -408,6 +413,90 @@ will not fail your overall build. Please see the [Travis CI Rust
 documentation](https://docs.travis-ci.com/user/languages/rust/) for more
 information.
 
+# Templates
+
+Cargo uses the [handlebars](https://github.com/sunng87/handlebars-rust) library
+to compile the templates used to scaffold projects. By default, there are only
+two templates available, `bin` and `lib`.  These are used by cargo to create the
+standard project structure.
+
+You can also specify other templates from which to scaffold your project. The
+`--template` argument to `cargo new` accepts either a path on your system, or a
+URL to a remote Git repository containing a project template.
+
+```
+# use the mytemplate template which is located in ~/.cargo/templates/mytemplate
+$ cargo new myproj --template ~/.cargo/mytemplates/mytemplate
+
+# download the template called mytemplate from your github package
+$ cargo new myproj --template http://github.com/you/mytemplate
+```
+
+If you have a collection of templates in a Git repository then you can use the
+`--template-subdir` option to specify the subdirectory containing the template
+you want to use.
+
+```
+# download the template project called mytemplates from your github package
+# and use the 'command-line-project' template.
+$ cargo new myproj --template http://github.com/you/mytemplate --template-subdir command-line-project
+```
+
+## Creating new templates
+
+A cargo template is just a folder containing one or more files. Usually, there
+is a `Cargo.toml` and a `src` directory. Each file in the template directory
+(aside from the contents of the .git directory) will be treated as a handlebars
+template. This means you can use handlebars variables wherever you want dynamic
+content, and cargo will render the proper values. Let's create a simple example.
+Create a new folder called `mytemplate`
+
+Add the following files:
+
+```toml
+# Cargo.toml
+[project]
+name = "{{name}}"
+version = "0.1.0"
+authors = [{{toml-escape author}}]
+```
+
+```rust
+// src/main.rs
+fn main() {
+    prinln!("This is the {{name}} project!");
+}
+```
+
+Upload this to a public git repository and anyone can now use it to start their
+projects with this command:
+
+```
+$ cargo new proj --template http://your/project/repo
+```
+
+## Available variables
+
+The variables available for use are:
+
+- `name`: the name of the project
+- `authors`: the toml formatted name of the project author
+
+In the future, more variables may be added. Suggestions welcome!
+
+## Available templating functions
+
+The available templating functions are:
+
+- `toml-escape`: Escapes a string for use in a TOML file.
+  file.
+- `html-escape`: Escapes a string for use in a HTML file.
+
+There is more documentation available on the [Handlebars
+website](http://handlebarsjs.com/) though keep in mind that [the Rust
+implementation of Handlebars](https://docs.rs/handlebars/0.24.1/handlebars/)
+isn't 100% compatible with the Javascript version.
+
 # Further reading
 
 Now that you have an overview of how to use cargo and have created your first crate, you may be interested in:
index 4a23bf35280d8439d6fbaff5bdb490fb27611281..d1b43182ef6c2c80965f87cae1b7ec0a07d32515 100644 (file)
@@ -200,6 +200,7 @@ case $state in
                     '--vcs:initialize a new repo with a given VCS:(git hg none)' \
                     '(-h, --help)'{-h,--help}'[show help message]' \
                     '--name=[set the resulting package name]' \
+                    '--template[template from which to scaffold your project]' \
                     '(-q, --quiet)'{-q,--quiet}'[no output printed to stdout]' \
                     '(-v, --verbose)'{-v,--verbose}'[use verbose output]' \
                     '--color=:colorization option:(auto always never)' \
index b68488dd4b226f3ba02ebe3f33a590c7fe380964..c902cb80fc6d8ea3cca80e0fb13674622642cd6d 100644 (file)
@@ -40,7 +40,7 @@ _cargo()
        local opt__locate_project="$opt_mani -h --help"
        local opt__login="$opt_common $opt_lock --host"
        local opt__metadata="$opt_common $opt_feat $opt_mani $opt_lock --format-version --no-deps"
-       local opt__new="$opt_common $opt_lock --vcs --bin --lib --name"
+       local opt__new="$opt_common $opt_lock --vcs --bin --lib --name --template"
        local opt__owner="$opt_common $opt_lock -a --add -r --remove -l --list --index --token"
        local opt__package="$opt_common $opt_mani $opt_lock $opt_jobs --allow-dirty -l --list --no-verify --no-metadata"
        local opt__pkgid="${opt__fetch} $opt_pkg"
index b0d4163f16cd0a9d8c55be199499b76161c8915c..7809f4892381139a5bceafbe9206d32989f0bb1b 100644 (file)
@@ -9,7 +9,7 @@ use std::env;
 
 use cargo::util::ProcessBuilder;
 use cargotest::process;
-use cargotest::support::{execs, paths};
+use cargotest::support::{execs, git, paths};
 use hamcrest::{assert_that, existing_file, existing_dir, is_not};
 use tempdir::TempDir;
 
@@ -55,6 +55,75 @@ fn simple_bin() {
                 existing_file());
 }
 
+#[test]
+fn simple_template() {
+    let root = paths::root();
+    fs::create_dir_all(&root.join("home/.cargo/templates/testtemplate/src")).unwrap();
+    File::create(&root.join("home/.cargo/templates/testtemplate/Cargo.toml"))
+                      .unwrap().write_all(br#"[package]
+name = "{{name}}"
+version = "0.0.1"
+authors = ["{{author}}"]
+"#).unwrap();
+    File::create(&root.join("home/.cargo/templates/testtemplate/src/main.rs"))
+                      .unwrap().write_all(br#"
+fn main () {
+  println!("hello {{name}}");
+}
+    "#).unwrap();
+
+    assert_that(cargo_process("new").arg("--template-subdir").arg("testtemplate")
+                                    .arg("--template")
+                                    .arg(&root.join("home/.cargo/templates/"))
+                                    .arg("foo")
+                                    .env("USER", "foo"),
+                execs().with_status(0).with_stderr("\
+[CREATED] library `foo` project
+"));
+
+    assert_that(&paths::root().join("foo"), existing_dir());
+    assert_that(&paths::root().join("foo/Cargo.toml"), existing_file());
+    assert_that(&paths::root().join("foo/src/main.rs"), existing_file());
+
+    assert_that(cargo_process("build").cwd(&paths::root().join("foo")),
+                execs().with_status(0));
+    assert_that(&paths::root().join(&format!("foo/target/debug/foo{}",
+                                             env::consts::EXE_SUFFIX)),
+                existing_file());
+}
+
+#[test]
+fn git_template() {
+    let git_project = git::new("template1", |project| {
+        project
+            .file("Cargo.toml", r#"[package]
+name = "{{name}}"
+version = "0.0.1"
+authors = ["{{author}}"]
+            "#)
+            .file("src/main.rs", r#"
+                pub fn main() {
+                    println!("hello world");
+                }
+            "#)
+    }).unwrap();
+
+    assert_that(cargo_process("new").arg("--template").arg(git_project.url().as_str())
+                                    .arg("foo")
+                                    .env("USER", "foo"),
+                execs().with_status(0));
+
+    assert_that(&paths::root().join("foo"), existing_dir());
+    assert_that(&paths::root().join("foo/Cargo.toml"), existing_file());
+    assert_that(&paths::root().join("foo/src/main.rs"), existing_file());
+
+    assert_that(cargo_process("build").cwd(&paths::root().join("foo")),
+                execs().with_status(0));
+    assert_that(&paths::root().join(&format!("foo/target/debug/foo{}",
+                                             env::consts::EXE_SUFFIX)),
+                existing_file());
+}
+
 #[test]
 fn both_lib_and_bin() {
     let td = TempDir::new("cargo").unwrap();